﻿// ===================================
// <copyright>LogoUI Co.</copyright>
// <author>Vlad Spivak</author>
// <email>mailto:vlads@logoui.co.il</email>
// <created>20/00/10</created>
// <lastedit>Tuesday, November 23, 2010</lastedit>

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the 'Software'), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//
// <remarks>Part of this software based on various internet sources, mostly on the works
// of members of Wpf Disciples group http://wpfdisciples.wordpress.com/
// Also project may contain code from works of Nito and OpenLight group</remarks>
// ====================================================================================//



using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace LogoFX.Controls
{
  /// <summary>
  /// Represents a zoombar with a preview content.
  /// </summary>
  [TemplatePart(Name = ZoomBar.ZoombarTemplateName, Type = typeof(FrameworkElement))]
  [TemplatePart(Name = ZoomBar.LowerPartName, Type = typeof(ColumnDefinition))]
  [TemplatePart(Name = ZoomBar.RangePartName, Type = typeof(ColumnDefinition))]
  [TemplatePart(Name = ZoomBar.UpperPartName, Type = typeof(ColumnDefinition))]
  [TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)]
  [TemplateVisualState(Name = VisualStates.StateMouseOver, GroupName = VisualStates.GroupCommon)]
  [TemplateVisualState(Name = VisualStates.StateDisabled, GroupName = VisualStates.GroupCommon)]
  [TemplateVisualState(Name = VisualStates.StateUnfocused, GroupName = VisualStates.GroupFocus)]
  [TemplateVisualState(Name = VisualStates.StateFocused, GroupName = VisualStates.GroupFocus)]
  public class ZoomBar : Control
  {
    ///////////////////////////////////////////////////////////////
    // Private members
    private bool m_isFirstTimeUpdate = true;
    private bool m_isUpdatedFromCode = true;
    private ReentranceGuard m_semaphore = new ReentranceGuard();

    ///////////////////////////////////////////////////////////////
    // Construction/Desctruction

    /// <summary>
    /// Initializes a new instance of the <see cref="ZoomBar"/> class.
    /// </summary>
    public ZoomBar()
    {
      this.DefaultStyleKey = typeof(ZoomBar);

      this.LayoutUpdated += OnLayoutUpdated;
    }

    ///////////////////////////////////////////////////////////////
    // Events

    ///////////////////////////////////////////////////////////////
    // Template Parts

    /// <summary>Zoombar template root.</summary>
    public virtual FrameworkElement ZoombarTemplate { get; set; }
    /// <summary>Gets or sets the part of the Grid corresponding to the <see cref="LowerProperty"/>.</summary>
    public ColumnDefinition LowerPart { get; set; }
    /// <summary>Gets or sets the part of the Grid corresponding to the <see cref="UpperProperty"/>.</summary>
    public ColumnDefinition UpperPart { get; set; }
    /// <summary>Gets or sets the part of the Grid corresponding to the (<see cref="UpperProperty"/> - <see cref="LowerProperty"/>).</summary>
    public ColumnDefinition RangePart { get; set; }

    internal const string ZoombarTemplateName = "ZoombarTemplate";
    internal const string LowerPartName = "LowerPart";
    internal const string UpperPartName = "UpperPart";
    internal const string RangePartName = "RangePart";

    ///////////////////////////////////////////////////////////////
    // Properties

    /// <summary>
    /// Gets a semaphore for locking the UI update.
    /// </summary>
    public ReentranceGuard UpdateLocker
    {
      get { return m_semaphore; }
    }

    /// <summary>
    /// Defines the Minimum dependency property.
    /// </summary>
    public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(
        "Minimum",
        typeof(double),
        typeof(ZoomBar),
        new PropertyMetadata(0.0, OnMinimumPropertyChanged));
    /// <summary>
    /// Gets or sets the Minimum value of the range.
    /// </summary>
    public double Minimum
    {
      get { return (double)GetValue(MinimumProperty); }
      set { SetValue(MinimumProperty, value); }
    }

    /// <summary>
    /// Defines the Maximum dependency property.
    /// </summary>
    public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(
        "Maximum",
        typeof(double),
        typeof(ZoomBar),
        new PropertyMetadata(1.0, OnMaximumPropertyChanged));
    /// <summary>
    /// Gets or sets the Minimum value of the range.
    /// </summary>
    public double Maximum
    {
      get { return (double)GetValue(MaximumProperty); }
      set { SetValue(MaximumProperty, value); }
    }

    /// <summary>
    /// Defines the Lower dependency property.
    /// </summary>
    public static readonly DependencyProperty LowerProperty = DependencyProperty.Register(
        "Lower",
        typeof(double),
        typeof(ZoomBar),
        new PropertyMetadata(0.2, OnLowerPropertyChanged));
    /// <summary>
    /// Gets or sets the Lower value of the range.
    /// </summary>
    public double Lower
    {
      get { return (double)GetValue(LowerProperty); }
      set { SetValue(LowerProperty, value); }
    }

    /// <summary>
    /// Defines the Upper dependency property.
    /// </summary>
    public static readonly DependencyProperty UpperProperty = DependencyProperty.Register(
        "Upper",
        typeof(double),
        typeof(ZoomBar),
        new PropertyMetadata(0.8, OnUpperPropertyChanged));
    /// <summary>
    /// Gets or sets the Upper value of the range.
    /// </summary>
    public double Upper
    {
      get { return (double)GetValue(UpperProperty); }
      set { SetValue(UpperProperty, value); }
    }

    /// <summary>
    /// Identifies the PreviewContent dependency property.
    /// </summary>
    public static readonly DependencyProperty PreviewContentProperty = DependencyProperty.Register(
        "PreviewContent",
        typeof(object),
        typeof(ZoomBar),
        null);
    /// <summary>
    /// Gets or sets preview content property for the ZoomBar control. This is a dependency property.
    /// </summary>
    public object PreviewContent
    {
      get { return base.GetValue(PreviewContentProperty); }
      set { base.SetValue(PreviewContentProperty, value); }
    }

    ///////////////////////////////////////////////////////////////
    // Overrides

    /// <summary>
    /// Called when an internal process or application calls ApplyTemplate, which is used to build the current template's visual tree.
    /// </summary>
    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      this.ZoombarTemplate = GetTemplateChild(ZoombarTemplateName) as FrameworkElement;
      this.LowerPart = GetTemplateChild(LowerPartName) as ColumnDefinition;
      this.UpperPart = GetTemplateChild(UpperPartName) as ColumnDefinition;
      this.RangePart = GetTemplateChild(RangePartName) as ColumnDefinition;
    }

    ///////////////////////////////////////////////////////////////
    // Implementation

    /// <summary>
    /// Checks if the provided value is a valid double.
    /// </summary>
    /// <param name="value">The value to be checked.</param>
    /// <returns>true if the vlaue is a valid double; otherwise false.</returns>
    private static bool IsValidDoubleValue(object value)
    {
      double number = (double)value;
      return (!double.IsNaN(number) && !double.IsInfinity(number));
    }

    /// <summary>
    /// Updates <see cref="LowerPart"/> and <see cref="UpperPart"/> elements accodring to <see cref="LowerProperty"/> and <see cref="UpperProperty"/> values.
    /// </summary>
    private void UpdateParts()
    {
      // Check id the UI update is locked then get out.
      if (this.UpdateLocker.IsLocked)
      {
        return;
      }
      // Check if all properties are valid. If one of the properties is not updated yed, we cannot update the UI.
      if (!IsValidDoubleValue(this.Minimum) || !IsValidDoubleValue(this.Lower) ||
          !IsValidDoubleValue(this.Maximum) || !IsValidDoubleValue(this.Upper))
      {
        return;
      }
      // Update UI elements.
      double totalLength = (this.ZoombarTemplate != null ? totalLength = this.ZoombarTemplate.ActualWidth : totalLength = this.ActualWidth);
      double rangeLowerAddition = 0;
      double rangeUpperAddition = 0;
      if (this.LowerPart != null)
      {
        rangeLowerAddition = (totalLength == 0 ? 0.0 : this.LowerPart.MinWidth / totalLength);
      }
      if (this.UpperPart != null)
      {
        rangeUpperAddition = (totalLength == 0 ? 0.0 : this.UpperPart.MinWidth / totalLength);
      }
      if (this.RangePart != null)
      {
        double length = ((this.Maximum - this.Minimum) - (this.Maximum - this.Upper) - (this.Lower - this.Minimum)) / (this.Maximum - this.Minimum);
        length -= length * (rangeLowerAddition + rangeUpperAddition);
        if (length < 0 || double.IsNaN(length))
        {
          length = 0;
        }
        if(double.IsInfinity(length))
        {
          length = 1;
        }
        this.RangePart.Width = new GridLength(length, GridUnitType.Star);
      }
      if (this.LowerPart != null)
      {
        double length = (this.Lower - this.Minimum) / (this.Maximum - this.Minimum);
        length += length * rangeLowerAddition;
        this.LowerPart.Width = new GridLength(double.IsNaN(length)?0:length, GridUnitType.Star);
      }
      if (this.UpperPart != null)
      {
        double length = (this.Maximum - this.Upper) / (this.Maximum - this.Minimum);
        length += length * rangeUpperAddition;
        this.UpperPart.Width = new GridLength(double.IsNaN(length) ? 0 : length, GridUnitType.Star);
      }
      m_isUpdatedFromCode = true;
    }

    /// <summary>
    /// Updates <see cref="LowerProperty"/> and <see cref="UpperProperty"/> members accodring to <see cref="LowerPart"/> and <see cref="UpperPart"/> elements.
    /// </summary>
    private void UpdateProperties()
    {
      // Check id the UI update is locked then get out.
      if (this.UpdateLocker.IsLocked)
      {
        return;
      }
      if (this.LowerPart == null || this.RangePart == null || this.UpperPart == null)
      {
        return;
      }
      GridLength lowerLength = this.LowerPart.Width;
      GridLength upperLength = this.UpperPart.Width;
      GridLength rangeLength = this.RangePart.Width;

      double totalLength = (this.ZoombarTemplate != null ? totalLength = this.ZoombarTemplate.ActualWidth : totalLength = this.ActualWidth);
      double rangeLowerAddition = (totalLength == 0 ? 0.0 : this.LowerPart.MinWidth / totalLength);
      double rangeUpperAddition = (totalLength == 0 ? 0.0 : this.UpperPart.MinWidth / totalLength);
      double total = lowerLength.Value + upperLength.Value + rangeLength.Value;
      if (total == 0)
      {
        return;
      }
      using (this.UpdateLocker.Raise())
      {
        // TODO: investigate why this code doesn't work
        //if ((lowerLength.Value / total * totalLength) <= this.LowerPart.MinWidth)
        //{
        //  this.Lower = this.Minimum;
        //}
        //else
        {
          this.Lower = this.Minimum + (this.Maximum - this.Minimum) * (lowerLength.Value - rangeLowerAddition) / total;
        }
        // TODO: investigate why this code doesn't work
        //if ((upperLength.Value / total * totalLength) <= this.UpperPart.MinWidth)
        //{
        //  this.Upper = this.Maximum;
        //}
        //else
        {
          this.Upper = this.Minimum +
                       ((this.Maximum - this.Minimum) -
                        (this.Maximum - this.Minimum) * (upperLength.Value - rangeUpperAddition) / total);
        }
      }
    }

    ///////////////////////////////////////////////////////////////
    // Event handlers

    /// <summary>
    /// The PropertyChangedCallback that is called when the <see cref="MinimumProperty"/> is changed.
    /// </summary>
    /// <param name="d">An instance of the <see cref="ZoomBar"/> object whose <see cref="MinimumProperty"/> was changed.</param>
    /// <param name="e">An instance of the DependencyPropertyChangedEventArgs class.</param>
    private static void OnMinimumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      // Validate the provided Minimum value.
      if (!IsValidDoubleValue(e.NewValue))
      {
        throw new ArgumentException("Invalid double value ", MinimumProperty.ToString());
      }
      double newMinimumValue = (double)e.NewValue;
      ZoomBar zoombar = d as ZoomBar;
      if (zoombar == null)
      {
        return;
      }
      using (zoombar.UpdateLocker.Raise())
      {
        // Check if the new Minimum value is bigger of the Maximum, then fix it.
        if (IsValidDoubleValue(zoombar.Maximum) && newMinimumValue > zoombar.Maximum)
        {
          zoombar.Maximum = newMinimumValue;
        }
        // Check if the Lower value is glued to Minimum, then update it too.
        if (IsValidDoubleValue(e.OldValue))
        {
          double oldMinimumValue = (double)e.OldValue;
          if (zoombar.Lower == oldMinimumValue)
          {
            zoombar.Lower = newMinimumValue;
          }
        }
        // Fix Lower and Upper values.
        if (IsValidDoubleValue(zoombar.Lower) && zoombar.Lower < newMinimumValue)
        {
          zoombar.Lower = newMinimumValue;
        }
        if (IsValidDoubleValue(zoombar.Upper) && zoombar.Upper < newMinimumValue)
        {
          zoombar.Upper = newMinimumValue;
        }
        // Update corresponding UI elements
        zoombar.UpdateParts();
      }
    }

    /// <summary>
    /// The PropertyChangedCallback that is called when the <see cref="MaximumProperty"/> is changed.
    /// </summary>
    /// <param name="d">An instance of the <see cref="ZoomBar"/> object whose <see cref="MaximumProperty"/> was changed.</param>
    /// <param name="e">An instance of the DependencyPropertyChangedEventArgs class.</param>
    private static void OnMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      // Validate the provided Maximum value.
      if (!IsValidDoubleValue(e.NewValue))
      {
        throw new ArgumentException("Invalid double value ", MaximumProperty.ToString());
      }
      double newMaximumValue = (double)e.NewValue;
      ZoomBar zoombar = d as ZoomBar;
      if (zoombar == null)
      {
        return;
      }
      using (zoombar.UpdateLocker.Raise())
      {
        // First check if the new Maximum value is smaller of the Minimum, then fix it.
        if (IsValidDoubleValue(zoombar.Minimum) && newMaximumValue < zoombar.Minimum)
        {
          zoombar.Minimum = newMaximumValue;
        }
        // Check if the Upper value is glued to Maximum, then update it too.
        if (IsValidDoubleValue(e.OldValue))
        {
          double oldMaximumValue = (double)e.OldValue;
          if (zoombar.Upper == oldMaximumValue)
          {
            zoombar.Upper = newMaximumValue;
          }
        }
        // Fix Lower and Upper values.
        if (IsValidDoubleValue(zoombar.Lower) && zoombar.Lower > newMaximumValue)
        {
          zoombar.Lower = newMaximumValue;
        }
        if (IsValidDoubleValue(zoombar.Upper) && zoombar.Upper > newMaximumValue)
        {
          zoombar.Upper = newMaximumValue;
        }
        // Update corresponding UI elements
        zoombar.UpdateParts();
      }
    }

    /// <summary>
    /// The PropertyChangedCallback that is called when the <see cref="LowerProperty"/> is changed.
    /// </summary>
    /// <param name="d">An instance of the <see cref="ZoomBar"/> object whose <see cref="LowerProperty"/> was changed.</param>
    /// <param name="e">An instance of the DependencyPropertyChangedEventArgs class.</param>
    private static void OnLowerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      // Validate the provided Minimum value.
      if (!IsValidDoubleValue(e.NewValue))
      {
        throw new ArgumentException("Invalid double value ", LowerProperty.ToString());
      }
      double newLowerValue = (double)e.NewValue;
      ZoomBar zoombar = d as ZoomBar;
      if (zoombar == null)
      {
        return;
      }
      using (zoombar.UpdateLocker.Raise())
      {
        // Check if the new Lower value is smaller of the Minimum or bigger than the Maximum.
        if (IsValidDoubleValue(zoombar.Minimum) && newLowerValue < zoombar.Minimum)
        {
          zoombar.Lower = zoombar.Minimum;
        }
        else if (IsValidDoubleValue(zoombar.Maximum) && newLowerValue > zoombar.Maximum)
        {
          zoombar.Lower = zoombar.Maximum;
        }
        // Check if the Lower value is bigger than the Upper value.
        if (IsValidDoubleValue(zoombar.Upper) && zoombar.Lower > zoombar.Upper)
        {
          zoombar.Upper = zoombar.Lower;
        }
        // Update corresponding UI elements
        zoombar.UpdateParts();
      }
    }

    /// <summary>
    /// The PropertyChangedCallback that is called when the <see cref="UpperProperty"/> is changed.
    /// </summary>
    /// <param name="d">An instance of the <see cref="ZoomBar"/> object whose <see cref="UpperProperty"/> was changed.</param>
    /// <param name="e">An instance of the DependencyPropertyChangedEventArgs class.</param>
    private static void OnUpperPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      // Validate the provided Minimum value.
      if (!IsValidDoubleValue(e.NewValue))
      {
        throw new ArgumentException("Invalid double value ", UpperProperty.ToString());
      }
      double newUpperValue = (double)e.NewValue;
      ZoomBar zoombar = d as ZoomBar;
      if (zoombar == null)
      {
        return;
      }
      using (zoombar.UpdateLocker.Raise())
      {
        // Check if the new Lower value is smaller of the Minimum or bigger than the Maximum.
        if (IsValidDoubleValue(zoombar.Maximum) && newUpperValue > zoombar.Maximum)
        {
          zoombar.Upper = zoombar.Maximum;
        }
        else if (IsValidDoubleValue(zoombar.Minimum) && newUpperValue < zoombar.Minimum)
        {
          zoombar.Upper = zoombar.Minimum;
        }
        // Check if the Upper value is less than the Lower value.
        if (IsValidDoubleValue(zoombar.Lower) && zoombar.Upper < zoombar.Lower)
        {
          zoombar.Lower = zoombar.Upper;
        }
        // Update corresponding UI elements
        zoombar.UpdateParts();
      }
    }

    ///////////////////////////////////////////////////////////////////

    /// <summary>
    /// Occurs when the layout of the various visual elements associated with the current <see cref="Dispatcher"/> changes.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">An <see cref="EventArgs"/> that contains no event data.</param>
    private void OnLayoutUpdated(object sender, EventArgs e)
    {
      if (m_isFirstTimeUpdate)
      {
        // Update layout for the first time.
        double totalLength = (this.ZoombarTemplate != null ? totalLength = this.ZoombarTemplate.ActualWidth : totalLength = this.ActualWidth);
        if (totalLength == 0)
        {
          return;
        }
        m_isFirstTimeUpdate = false;
        UpdateParts();
        return;
      }
      if (this.Visibility != System.Windows.Visibility.Visible)
      {
        return;
      }
      if (!this.IsEnabled)
      {
        return;
      }
      if (m_isUpdatedFromCode)
      {
        // Prevent double layout update after redraing parts.
        m_isUpdatedFromCode = false;
        return;
      }
      //this.Dispatcher.BeginInvoke(UpdateProperties);
      UpdateProperties();
    }
  }
}
